﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using Newtonsoft.Json.Linq;

namespace RuntimeTypeGenerator
{
    /// <summary>
    /// Defines a type builder from a JSON data sample.
    /// </summary>
    public class JsonSourceTypeBuilder : ITypeBuilder
    {
        /// <summary>
        /// Gets the name used to identify the type produced by this builder.
        /// </summary>
        public string TypeName { get; private set; }

        /// <summary>
        /// Gets the JSON data sample.
        /// </summary>
        public JToken Source { get; private set; }

        /// <summary>
        /// Defines a type builder from a JSON data sample.
        /// </summary>
        /// <param name="typeName">The type name.</param>
        /// <param name="source">The JSON data sample.</param>
        public JsonSourceTypeBuilder(string typeName, JToken source)
        {
            if (string.IsNullOrEmpty(typeName))
            {
                throw new ArgumentNullException("typeName");
            }
            if (source == null)
            {
                throw new ArgumentNullException("source");
            }

            TypeName = typeName;
            Source = source;
        }

        /// <summary>
        /// Builds the type into the provided <paramref name="moduleBuilder"/>.
        /// </summary>
        /// <param name="moduleBuilder">The target of the type build.</param>
        /// <returns>The built type.</returns>
        public Type BuildType(ModuleBuilder moduleBuilder)
        {
            if (moduleBuilder == null)
            {
                throw new ArgumentNullException("moduleBuilder");
            }

            return BuildType(moduleBuilder, moduleBuilder.Assembly.GetName().Name + "." + TypeName, Source);
        }

        private static Type BuildType(ModuleBuilder moduleBuilder, string name, JToken raw)
        {
            switch (raw.Type)
            {
                case JTokenType.Object:
                    return BuildTypeFromObject(moduleBuilder, name, raw as JObject);
                case JTokenType.Array:
                    return BuildTypeFromArray(moduleBuilder, name, raw as JArray);
                default:
                    return BuildSimpleType(raw.Type);
            }
        }

        private static Type BuildTypeFromArray(ModuleBuilder moduleBuilder, string name, JArray jArray)
        {
            // unknown or various inner types
            if (jArray.Count == 0 || !AreAllSameType(jArray))
            {
                return typeof(Array);
            }

            // array of elemnt type at position 0
            return BuildType(moduleBuilder, name, jArray[0]).MakeArrayType();
        }

        private static bool AreAllSameType(IEnumerable<JToken> tokens)
        {
            return (tokens.Select(t => t.Type).Distinct().Count() == 1);
        }

        private static Type BuildSimpleType(JTokenType tokenType)
        {
            switch (tokenType)
            {
                case JTokenType.Boolean:
                    return typeof(bool);
                case JTokenType.Bytes:
                    return typeof(byte[]);
                case JTokenType.Date:
                    return typeof(DateTime);
                case JTokenType.Float:
                    return typeof(Double);
                case JTokenType.Guid:
                    return typeof(Guid);
                case JTokenType.Integer:
                    return typeof(Int32);
                case JTokenType.None:
                case JTokenType.Null:
                    return typeof(object);
                case JTokenType.String:
                case JTokenType.Uri:
                    return typeof(String);
                case JTokenType.TimeSpan:
                    return typeof(TimeSpan);
                default:
                    throw new ArgumentException("Not supported simple type " + tokenType);
            }
        }

        private static Type BuildTypeFromObject(ModuleBuilder moduleBuilder, string name, JObject jObject)
        {
            var typeBuilder = moduleBuilder.DefineType(name, TypeAttributes.Public | TypeAttributes.Class | TypeAttributes.Serializable);
            // define a field for each property
            foreach (var prop in jObject.Properties())
            {
                typeBuilder.DefineField(prop.Name, BuildType(moduleBuilder, name + "_" + prop.Name, prop.Value), FieldAttributes.Public);
            }
            // public default ctor
            typeBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, null)
                       .GetILGenerator()
                       .Emit(OpCodes.Ret);
            // create the actual type
            return typeBuilder.CreateType();
        }
    }
}
